/*
 *  Copyright 2011 Iurii Dunko, Iana Potochniak.
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *  under the License.
 */

package org.opu.yyminijava.visitor;

import static org.opu.yyminijava.visitor.TypeCheckException.*;
import java.util.ArrayList;
import java.util.List;
import org.opu.yyminijava.symboltable.ClassRecord;
import org.opu.yyminijava.symboltable.MethodRecord;
import org.opu.yyminijava.Part;
import org.opu.yyminijava.symboltable.SymbolTable;
import org.opu.yyminijava.symboltable.VarRecord;
import org.opu.yyminijava.syntaxtree.ArgumentList;
import org.opu.yyminijava.syntaxtree.ArithmeticalAddExpression;
import org.opu.yyminijava.syntaxtree.ArithmeticalAddOp;
import org.opu.yyminijava.syntaxtree.ArithmeticalMullExpression;
import org.opu.yyminijava.syntaxtree.ArithmeticalMullOp;
import org.opu.yyminijava.syntaxtree.ArrayAccess;
import org.opu.yyminijava.syntaxtree.ArrayType;
import org.opu.yyminijava.syntaxtree.BooleanExpression;
import org.opu.yyminijava.syntaxtree.BooleanOp;
import org.opu.yyminijava.syntaxtree.ClassBody;
import org.opu.yyminijava.syntaxtree.ClassType;
import org.opu.yyminijava.syntaxtree.CompareExpression;
import org.opu.yyminijava.syntaxtree.CompareOp;
import org.opu.yyminijava.syntaxtree.Expression;
import org.opu.yyminijava.syntaxtree.FieldInvocation;
import org.opu.yyminijava.syntaxtree.FormalParameter;
import org.opu.yyminijava.syntaxtree.LogicalExpression;
import org.opu.yyminijava.syntaxtree.LogicalOp;
import org.opu.yyminijava.syntaxtree.MethodCall;
import org.opu.yyminijava.syntaxtree.MethodDeclaration;
import org.opu.yyminijava.syntaxtree.MethodInvocation;
import org.opu.yyminijava.syntaxtree.Name;
import org.opu.yyminijava.syntaxtree.PostfixExpression;
import org.opu.yyminijava.syntaxtree.PrefixExpression;
import org.opu.yyminijava.syntaxtree.PrefixOp;
import org.opu.yyminijava.syntaxtree.PrimitiveType;
import org.opu.yyminijava.syntaxtree.Suffix;
import org.opu.yyminijava.syntaxtree.Type;
import org.opu.yyminijava.syntaxtree.primary.Primary;
import org.opu.yyminijava.syntaxtree.primary.PrimaryFromExpression;
import org.opu.yyminijava.syntaxtree.primary.PrimaryFromIdentifier;
import org.opu.yyminijava.syntaxtree.primary.PrimaryFromLiteral;
import org.opu.yyminijava.syntaxtree.primary.PrimaryFromNewInstance;
import org.opu.yyminijava.syntaxtree.primary.PrimaryFromThis;
import org.opu.yyminijava.syntaxtree.primary.PrimaryMethodCall;

/**
 *
 * @author Iurii Dunko
 * @author Iana  Potochniak
 */
public class TypeCheckingHelper {

    private static final PrimitiveType tint = PrimitiveType.INT;
    private static final PrimitiveType tbool = PrimitiveType.BOOL;

    private SymbolTable symbolTable;
    private Object tmpExp;

    // TODO: REFACTOR ! ! ! (hardcode impl.)
    // TODO: remove tmpExp and do all correct !

    public TypeCheckingHelper(SymbolTable symbolTable, Object tmpExp) {
        this.symbolTable = symbolTable;
        this.tmpExp = tmpExp;
    }

    public TypeCheckingHelper setExpression(Object tmpExp){
        this.tmpExp = tmpExp;
        return this;
    }
    
    public Type confirmType(Expression expression){
        // Expression := BooleanExpression
        tmpExp = expression;
        return confirmType(expression.getBoolExp());
    }

    private Type confirmType(BooleanExpression expression){
        // BooleanExpression := LogicalExpression (BooleanOp LogicalExpression)*
        //                      '||' / '&&' only suport now
        // if it has two part it must return bool type, and each part muyst be
        // bool type

        LogicalExpression lgExp = expression.getLogExp();
        Type type = confirmType(lgExp);
        List<Part<BooleanOp, LogicalExpression>> rp = expression.rightPart();

        String errMsg = "Boolean operation acceptable only for boolean statements (" + tmpExp + ")";
        if (!rp.isEmpty()) {
            assertEquals(tbool, type, errMsg + "; But got " + type);
            for (Part<BooleanOp, LogicalExpression> part : rp) {
                type = confirmType(part.expression);
                assertEquals(tbool, type, errMsg + "; But got " + type);
            }
        }
        return type;
    }

    private Type confirmType(LogicalExpression expression){
        // LogicalExpression := CompareExpression (LogicalOp CompareExpression)*
        //                      '==' / '!=' only suport now
        // if it has two part it must return bool type, and each part can be
        // as bool type as int type; but we can't compare bool and int

        CompareExpression cprExp = expression.getCmprExp();
        Type type = confirmType(cprExp);
        List<Part<LogicalOp, CompareExpression>> rp = expression.rightPart();

        if (!rp.isEmpty()) {
            for (Part<LogicalOp, CompareExpression> part : rp) {
                Type t2 = confirmType(part.expression);
                assertEquals(type, t2, "Can't compare incomparable statement " 
                		+ type + " and " + t2 + "(" + tmpExp + ")");
                
                // after comparing we will always have bool type.
                type = tbool; 
            }
        }
        return type;
    }

    private Type confirmType(CompareExpression expression){
        // BooleanExpression := ArithmeticalAddExpression
        //                      (CompareOp ArithmeticalAddExpression)*
        //                      '>' / '<' / '=>' / '<=' only suport now
        // if it has two part it must return bool type, bute it containe from
        // int parts !

        ArithmeticalAddExpression addExp = expression.getArAddExp();
        Type type = confirmType(addExp);
        List<Part<CompareOp, ArithmeticalAddExpression>> rp = expression.rightPart();

        String errMsg = "Compare operation acceptable only for int statements (" + tmpExp + ")";
        if (!rp.isEmpty()) {
            assertEquals(tint, type, errMsg + "; But got " + type);
            
            for (Part<CompareOp, ArithmeticalAddExpression> part : rp) {
                type = confirmType(part.expression);
                assertEquals(tint, type, errMsg + "; But got " + type);
            }
            
            type = tbool;
        }
        return type;
    }

    private Type confirmType(ArithmeticalAddExpression expression){
        // BooleanExpression := ArithmeticalMullExpression
        //                      (ArithmeticalAddOp ArithmeticalMullExpression)*
        //                      '+' / '-' only suport now
        // if it has two part it must return int type

        ArithmeticalMullExpression mullExp = expression.getArMullExp();
        Type type = confirmType(mullExp);
        List<Part<ArithmeticalAddOp, ArithmeticalMullExpression>> rp = expression.rightPart();

        String errMsg = "Wrong type for arrifmatical expression (" + tmpExp + ")";
        if (!rp.isEmpty()) {
        	assertTrue(tint.equals(type) || isStr(type), errMsg);
            for (Part<ArithmeticalAddOp, ArithmeticalMullExpression> part : rp) {
                Type t2 = confirmType(part.expression);
                if (ArithmeticalAddOp.PLUS.equals(part.operation)
                        && (isStr(type) || isStr(t2))){
                    type = new ClassType(new Name("String"));
                } else {
                    type = t2;
                    assertEquals(tint, type, errMsg);
                }
            }
        }
        return type;
    }

    private Type confirmType(ArithmeticalMullExpression expression){
        // BooleanExpression := PrefixExpression
        //                      (ArithmeticalMullOp PrefixExpression)*
        //                      '*' / '/' only suport now
        // if it has two part it must return int type
        
        PrefixExpression prefxExp = expression.getPrefxExp();
        Type type = confirmType(prefxExp);
        List<Part<ArithmeticalMullOp, PrefixExpression>> rp = expression.rightPart();

        String errMsg = "Wrong type for multiplication expression (" + tmpExp + ")";
        if (!rp.isEmpty()) {
            assertEquals(tint, type, errMsg);
            for (Part<ArithmeticalMullOp, PrefixExpression> part : rp) {
                type = confirmType(part.expression);
                assertEquals(tint, type, errMsg);
            }
        }
        return type;
    }

    private Type confirmType(PrefixExpression expression){
        // BooleanExpression := PrefixOp ? PostfixExpression
        PrefixOp prefixOp = expression.getPrefixOp();
        Type type = confirmType(expression.getPostfixExpression());

        if (prefixOp != null){
            if (PrefixOp.NEGATION.equals(prefixOp)) {
                assertEquals(tint, type, "expect int but got " + type 
                		+ "(" + tmpExp + ")");
            } else if (PrefixOp.DENIAL.equals(prefixOp)) {
                assertEquals(tbool, type, "expect boolean but got " + type 
                		+ "(" + tmpExp + ")");
            } else {
                throw new IllegalArgumentException("unknown prefix op: " 
                		+ prefixOp + "(" + tmpExp + ")");
            }
        }

        return type;
    }

    public Type confirmType(PostfixExpression expression){
        // BooleanExpression := Primary (Suffix)*
        // previous type
        Type privTp = getPrimaryType(expression.getPrimary());

        for(Suffix suffix : expression.getSuffixs()){
            assertNotNull(privTp, "Call void type (" + tmpExp + ")");
            if (suffix instanceof MethodInvocation){
                MethodCall mc = ((MethodInvocation) suffix).getMethodCall();
                try {
                    // 1 create method record and list of method arguments
                    ArgumentList argumentList = mc.getArgumentList();
                    List<FormalParameter> list = new ArrayList<FormalParameter>();
                    List<Type> types = new ArrayList<Type>();
                    if (argumentList != null) {
                        for (Expression arg : argumentList.getExpressions()) {
                            Type confirmType = confirmType(arg);
                            types.add(confirmType);
                            list.add(new FormalParameter(confirmType, "@"));
                        }
                    }

                    MethodRecord mr = new MethodRecord(mc.getSimpleName(), privTp, list);

                    // 2 get class for which this method was caled
                    ClassRecord cr = symbolTable.getClass(((ClassType) privTp).getName());
                    assertNotNull(cr, "Cant't call " + mr + " from " + privTp);

                    // 2 get method and check it
                    ClassBody cb = cr.getValue().getClassBody();
                    MethodDeclaration m = cb.getMethod(mc.getSimpleName(), types);
                    assertNotNull(m, "Cant't call " + cr + "." + mc);

                    privTp = m.getHeader().getType();
                } catch (Exception e) {
                    throw new TypeCheckException("Cant't call " + mc + " from "
                            + privTp + " => (" + tmpExp + ")", e);
                }
            } else if (suffix instanceof FieldInvocation){
                String sname = ((FieldInvocation) suffix).getSimpleName();
                try {
                    // TODO: Create data type array that have attribute 'length' only!
                    // FIXME: Create data type array that have attribute 'length' only!
                    // ERROR: Create data type array that have attribute 'length' only!
		            if (privTp instanceof ArrayType){
	                    assertEquals("length", sname, "Wrong attribute " + sname
                                + "for array (" + tmpExp + ")");
	                    privTp = ((ArrayType)privTp).getType();
	                } else {
			            ClassRecord tp = symbolTable.getClass(privTp.getName());
        	            privTp = tp.getValue().getClassBody().getFieldType(sname);
                    }
                } catch (Exception e) {
                    throw new TypeCheckException("Failed call field " + sname 
                    		+ " from " + privTp + " => (" + tmpExp + ")", e);
                }
            } else if (suffix instanceof ArrayAccess){
                ArrayAccess arr = (ArrayAccess) suffix;
                Type tp = confirmType(arr.getExpression());

                assertEquals(tint, tp, "Rong attribute array index (" + tmpExp + ")");
                assertTrue(privTp instanceof ArrayType, "operation [] available "
                        + "only for array (" + tmpExp + ")");

                privTp =((ArrayType) privTp).getType();
            }
        }
        return privTp;
    }

    private Type getPrimaryType(Primary p){
        if (p instanceof PrimaryFromExpression){
            // If it expression we must to check this expression
            return confirmType(((PrimaryFromExpression) p).getExpression());
        } else if (p instanceof PrimaryFromIdentifier){
            // IF it identifier we try to find it in sybol table. At first we
            // looking for variable if we didn't found it we look for a class
            String name = ((PrimaryFromIdentifier) p).getSimpleName();

            try {
                return symbolTable.lookup(new VarRecord(name)).getType();
            } catch (Exception e) {
            }
            try {
                return symbolTable.getClass(name).getType();
            } catch (Exception e) {
                throw new TypeCheckException("" + p + " => (" + tmpExp + ")", e);
            }
        } else if (p instanceof PrimaryFromLiteral){
            // If it literal we return literal type
            return ((PrimaryFromLiteral) p).getLiteral().getType();
        } else if (p instanceof PrimaryFromNewInstance){
            // If it "new Object" we return type of creating object
            return ((PrimaryFromNewInstance) p).getNewInstance()
                    .getCreationExpression().getType();
        } else if (p instanceof PrimaryFromThis){
            // If it call this
            try {
                return symbolTable.getThis().getType();
            } catch (Exception e) {
                throw new TypeCheckException("" + p + " => (" + tmpExp + ")", e);
            }
        } else if (p instanceof PrimaryMethodCall){
            // If it method call
            MethodCall mc = ((PrimaryMethodCall) p).getMethodCall();
            return checkMethodCall(mc);
        } else {
            throw new TypeCheckException(new IllegalArgumentException("" + p
                    + " => (" + tmpExp + ")"));
        }
    }

    public Type checkMethodCall(MethodCall mc) {
        List<Expression> params;

        if (mc.getArgumentList() == null) {
            params = new ArrayList<Expression>();
        } else {
            params = mc.getArgumentList().getExpressions();
        }

        List<FormalParameter> fp = new ArrayList<FormalParameter>();
        for (int i = 0; i < params.size(); i++) {
            fp.add(new FormalParameter(confirmType(params.get(i)), "@"));
        }

        MethodRecord r = new MethodRecord(mc.getSimpleName(), null, fp);
        try {
            return symbolTable.lookup(r).getType();
        } catch (Exception e) {
            throw new TypeCheckException(e + " method not found - " + mc + " (" 
            		+ tmpExp + ")", e);
        }
    }

    private boolean isStr(Type t1) {
        if (t1 instanceof ClassType) {
            String name = ((ClassType) t1).getName();
            if ("String".equals(name) || "java.lang.String".equals(name)) {
                return true;
            }
        }
        return false;
    }
}
